/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package pl.edu.agh.student.nn.core.kohonen;

import pl.edu.agh.student.nn.core.*;

/**
 * @author janburkot
 */
public class KohonenLayer extends Layer {
    private int dimension;
    private int height;
    private int width;

    public KohonenLayer(int width, int height) {
        super(width * height, LayerType.KOH);
        this.width = width;
        this.height = height;
        this.dimension = width == 1 || height == 1 ? 1 : 2;
    }

    public void add(KohonenNeuron neuron) {
        super.getNeurons().add(neuron);
    }

    @Override
    public void calculate() {
        KohonenNeuron winner = findWinner(-1);
        for (Neuron n : getNeurons()) {
            if (n == winner) {
                n.setOutput(1.0);
            } else {
                n.setOutput(0.0);
            }
        }
    }

    @Override
    public void learn(int currentIteration, TrainingSet trainingSet) {
        KohonenNeuron winner = findWinner(currentIteration);
        adjustFrequency(winner, 1.0, currentIteration);
        adjustNeuronWeights(winner, winner, currentIteration);

        for (Neuron neuron : getNeurons()) {
            KohonenNeuron kn = (KohonenNeuron) neuron;
            adjustFrequency(kn, 0.0, currentIteration);
            if (areNeighbours(winner, kn, currentIteration)) {
                adjustNeuronWeights(kn, winner, currentIteration);
            }
        }
    }

    //szukanie zwyciescy najblizszego neuronu do wejscia
    private KohonenNeuron findWinner(int currentIteration) {
        KohonenNeuron winner = null;
        double min = Double.MAX_VALUE;
        for (Neuron neuron : getNeurons()) {
            KohonenNeuron kohonenNeuron = (KohonenNeuron) neuron;
            double distance = kohonenNeuron.getDistance();
            if (currentIteration >= 0) {
                distance += bias(kohonenNeuron, currentIteration);
            }
            if (distance < min) {
                min = distance;
                winner = kohonenNeuron;
            }
        }
        return winner;
    }

    private void adjustNeuronWeights(KohonenNeuron kn, KohonenNeuron winner, int currentIteration) {
        for (int i = 0; i < kn.getInputConnections().size(); i++) {
            double w_ij_old = kn.getInputConnections().get(i).getWeight();
            double x_j = kn.getInputConnections().get(i).getFromNeuron().getOutput();
            double alpha = getDoubleParam(LayerParameter.ALPHA, currentIteration);
            double neighbourhoodFunction = neighbourhoodFunction(kn, winner, currentIteration);
            double w_ij_new = w_ij_old + neighbourhoodFunction * alpha * (x_j - w_ij_old);

            kn.getInputConnections().get(i).setWeight(w_ij_new);
        }
    }

    //SOM4.jpg: B_i = gamma*(N*F_i - 1.0);
    private double bias(KohonenNeuron kn, int currentIteration) {
        double gamma = getDoubleParam(LayerParameter.CONSC, currentIteration);
        return gamma * (getNeurons().size() * kn.getF() - 1.0);
    }

    //na poczatku: F_i = 1/N
    //pozniej, zwyciezca: F_i += beta*(1.0 - F_i), reszta: F_i += beta*(0.0 - F_i)
    private void adjustFrequency(KohonenNeuron kn, double value, int currentIteration) {
        double beta = getDoubleParam(LayerParameter.BETA, currentIteration);
        double newF = kn.getF() + beta * (value - kn.getF());
        kn.setF(newF);
    }

    private boolean areNeighbours(KohonenNeuron winner, KohonenNeuron kn, int currentIteration) {
        if (winner == kn) {
            return false;
        }
        double radius = getDoubleParam(LayerParameter.RADIUS, currentIteration);
        double distance = distance(winner, kn);
        return distance <= radius;
    }

    //e^(-dist^2/(2*NEIGHBOURHOOD_WIDTH^2))
    private double neighbourhoodFunction(KohonenNeuron kn, KohonenNeuron winner, int currentIteration) {
        if (kn == winner)
            return 1.0;
        double neighbourhoodWidth = getDoubleParam(LayerParameter.NEIGH, currentIteration);
        double distance = distance(kn, winner);
        return neighbourhoodWidth == 0.0 ? 0.0
                : Math.exp(-1.0 * Math.pow(distance, 2.0)
                / (2 * Math.pow(neighbourhoodWidth, 2.0)));
    }

    private double distance(KohonenNeuron a, KohonenNeuron b) {
        double dist = 0.0;
        if (dimension == 2)
            dist = Math.pow(a.getV().get(0) - b.getV().get(0), 2)
                    + Math.pow(a.getV().get(1) - b.getV().get(1), 2);
        else
            dist = Math.abs(a.getV().get(0) - b.getV().get(0))
                    + Math.abs(a.getV().get(1) - b.getV().get(1));
        return dist;
    }


    public void display3x3() {
        for (Neuron n : getNeurons()) {
            KohonenNeuron kn = (KohonenNeuron) n;
            int i = 1;
            for (Connection c : kn.getInputConnections()) {
                System.out.printf("%.2f ", c.getWeight());
                if (i++ % 3 == 0)
                    System.out.println();
            }
            System.out.println("=======================");
        }
    }

    public int getDimension() {
        return dimension;
    }

    public void setDimension(int dimension) {
        this.dimension = dimension;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }
}
